#include <linux/string.h>
#include "tinf.h"
#include "zipfile.h"

enum {
    // finding the directory
    CD_SIGNATURE = 0x06054b50,
    EOCD_LEN     = 22,        // EndOfCentralDir len, excl. comment
    MAX_COMMENT_LEN = 65535,
    MAX_EOCD_SEARCH = MAX_COMMENT_LEN + EOCD_LEN,

    // central directory entries
    ENTRY_SIGNATURE = 0x02014b50,
    ENTRY_LEN = 46,          // CentralDirEnt len, excl. var fields

    // local file header
    LFH_SIZE = 30,
};

static int zipfile_read_central_dir_init(Zipfile* file);
static int zipfile_read_central_directory_entry(Zipfile* file, Zipentry* entry,
                const unsigned char** buf, unsigned int* len);

static unsigned int zipfile_read_le_int(const unsigned char* buf)
{
    return buf[0] | (buf[1] << 8) | (buf[2] << 16) | (buf[3] << 24);
}

static unsigned int zipfile_read_le_short(const unsigned char* buf)
{
    return buf[0] | (buf[1] << 8);
}

int zipfile_init(Zipfile* file, const void* data, size_t size, const char* fileName)
{
	int ret = OK;
	
	if (file ==  NULL)
	{
		ret = SOURCE_FILE_NULL_ERROR;
 		return ret;
	}
	
	file->buf = data;
    file->bufsize = size;
    ret = zipfile_read_central_dir_init(file);
    
    return ret;
}

int zipfile_beging(Zipfile* file)
{
	int ret = OK;
	
	file->entryIndex = 0;
	if (!zipfile_is_end(file))
	{
		ret = zipfile_advance(file);
	}

	return ret;
}

unsigned char zipfile_is_end(Zipfile* file)
{
	unsigned char isEnd = 0;

	if (file->entryIndex > file->totalEntryCount)
	{
		isEnd = 1;
    }

    return isEnd;
}

int zipfile_advance(Zipfile* file)
{
	int ret = OK;
	unsigned int len;
	
	file->entryIndex++;
	len = file->centralDirOffest;
	
    ret = zipfile_read_central_directory_entry(file, &(file->entries), &(file->curEntryOffset), &len);
    
    return ret;
}


int zipfile_decompress_zipentry(Zipentry* entry, void* buf, int bufsize)
{
     return tinf_uncompress(buf, (unsigned int*)&bufsize, entry->data, entry->compressedSize);
}

static int
zipfile_read_central_dir_values(Zipfile* file, const unsigned char* buf, int len)
{
	int ret = OK;
	
    if (len < EOCD_LEN) {
        ret = EOCD_LEN_ERROR;
        return ret;
    }

    file->disknum = zipfile_read_le_short(&buf[0x04]);
    file->diskWithCentralDir = zipfile_read_le_short(&buf[0x06]);
    file->entryCount = zipfile_read_le_short(&buf[0x08]);
    file->totalEntryCount = zipfile_read_le_short(&buf[0x0a]);
    file->centralDirSize = zipfile_read_le_int(&buf[0x0c]);
    file->centralDirOffest = zipfile_read_le_int(&buf[0x10]);
    file->commentLen = zipfile_read_le_short(&buf[0x14]);

    if (file->commentLen > 0) {
        if (EOCD_LEN + file->commentLen > len) {
            ret  = COMMET_LEN_ERROR;
            return ret;
        }
        file->comment = buf + EOCD_LEN;
    }

    return ret;
}

static int
zipfile_read_central_directory_entry(Zipfile* file, Zipentry* entry,
                const unsigned char** buf, unsigned int* len)
{
    const unsigned char* p;

    //unsigned short  versionMadeBy;
    //unsigned short  versionToExtract;
    //unsigned short  gpBitFlag;
    //unsigned short  lastModFileTime;
    //unsigned short  lastModFileDate;
    //unsigned long   crc32;
    unsigned short  extraFieldLength;
    unsigned short  fileCommentLength;
    //unsigned short  diskNumberStart;
    //unsigned short  internalAttrs;
    //unsigned long   externalAttrs;
    unsigned long   localHeaderRelOffset;
    //const unsigned char*  extraField;
    //const unsigned char*  fileComment;
    unsigned int dataOffset;
    int ret = OK;


    p = *buf;

    if (*len < ENTRY_LEN) {
    	ret = ENTRY_LEN_ERROR;
        return ret;
    }

    if (zipfile_read_le_int(&p[0x00]) != ENTRY_SIGNATURE) {
        ret = ENTRY_SIGNATURE_ERROR;
        return ret;
    }

    //versionMadeBy = zipfile_read_le_short(&p[0x04]);
    //versionToExtract = zipfile_read_le_short(&p[0x06]);
    //gpBitFlag = zipfile_read_le_short(&p[0x08]);
    entry->compressionMethod = zipfile_read_le_short(&p[0x0a]);
    //lastModFileTime = zipfile_read_le_short(&p[0x0c]);
    //lastModFileDate = zipfile_read_le_short(&p[0x0e]);
    //crc32 = zipfile_read_le_int(&p[0x10]);
    entry->compressedSize = zipfile_read_le_int(&p[0x14]);
    entry->uncompressedSize = zipfile_read_le_int(&p[0x18]);
    entry->fileNameLength = zipfile_read_le_short(&p[0x1c]);
    extraFieldLength = zipfile_read_le_short(&p[0x1e]);
    fileCommentLength = zipfile_read_le_short(&p[0x20]);
    //diskNumberStart = zipfile_read_le_short(&p[0x22]);
    //internalAttrs = zipfile_read_le_short(&p[0x24]);
    //externalAttrs = zipfile_read_le_int(&p[0x26]);
    localHeaderRelOffset = zipfile_read_le_int(&p[0x2a]);

    p += ENTRY_LEN;

    // filename
    if (entry->fileNameLength != 0) {
        entry->fileName = p;
    } else {
        entry->fileName = NULL;
    }
    p += entry->fileNameLength;

    // extra field
    if (extraFieldLength != 0) {
        //extraField = p;
    } else {
        //extraField = NULL;
    }
    p += extraFieldLength;

    // comment, if any
    if (fileCommentLength != 0) {
        //fileComment = p;
    } else {
        //fileComment = NULL;
    }
    p += fileCommentLength;

    *buf = p;

    // the size of the extraField in the central dir is how much data there is,
    // but the one in the local file header also contains some padding.
    p = file->buf + localHeaderRelOffset;
    extraFieldLength = zipfile_read_le_short(&p[0x1c]);

    dataOffset = localHeaderRelOffset + LFH_SIZE
        + entry->fileNameLength + extraFieldLength;
    entry->data = file->buf + dataOffset;
#if 0
    printf("file->buf=%p entry->data=%p dataOffset=%x localHeaderRelOffset=%d\n "
           "entry->fileNameLength=%d extraFieldLength=%d, entry->fileName:%s\n",
           file->buf, entry->data, dataOffset, localHeaderRelOffset,
           entry->fileNameLength, extraFieldLength, entry->fileName);
#endif
    return ret;
}

static int zipfile_read_central_dir_init(Zipfile* file)
{
    int ret = OK;
    const unsigned char* buf = file->buf;
    unsigned int bufsize = file->bufsize;
    const unsigned char* eocd;
    const unsigned char* p;
    const unsigned char* start;
    
    // too small to be a ZIP archive?
    if (bufsize < EOCD_LEN) {
        ret = EOCD_LEN_ERROR;
        return ret;
    }

    // find the end-of-central-dir magic
    if (bufsize > MAX_EOCD_SEARCH) {
        start = buf + bufsize - MAX_EOCD_SEARCH;
    } else {
        start = buf;
    }
    p = buf + bufsize - 4;
    while (p >= start) {
        if (*p == 0x50 && zipfile_read_le_int(p) == CD_SIGNATURE) {
            eocd = p;
            break;
        }
        p--;
    }
    if (p < start) {
        ret = EOCD_NOT_FOUND_ERROR;
        return ret;
    }

    // extract eocd values
    ret = zipfile_read_central_dir_values(file, eocd, (buf+bufsize)-eocd);
    if (ret != OK) {
        return ret;
    }

    if (file->disknum != 0
          || file->diskWithCentralDir != 0
          || file->entryCount != file->totalEntryCount) {
        ret = ARCHIVE_ERROR;
        return ret;
    }
    
    file->curEntryOffset = buf + file->centralDirOffest;
    return ret;
}
